﻿using System;
using System.Collections.Generic;
using System.Data;

namespace Lookstone.Data
{
	/// <summary>
	/// Generic row reader class (reads either a IDataReader column or DataRow column). Keeps a cache of the fields that are read
	/// so that it is more efficient to read multiple rows with string field indices
	/// </summary>
	public class RowReader
	{
		private System.Data.IDataReader _reader = null;
		private System.Data.DataRow _row = null;
		private Dictionary<string, int> _columnMap = null;

		/// <summary>
		/// Constructs a RowReader object using an IDataReader object
		/// </summary>
		/// <param name="reader">An IDataReader object to read from</param>
		public RowReader(System.Data.IDataReader reader)
		{
			_reader = reader;
		}

		/// <summary>
		/// Constructs a RowReader object using a DataRow object
		/// </summary>
		/// <param name="reader">A DataRow object to read from</param>
		public RowReader(System.Data.DataRow row)
		{
			_row = row;
		}

		/// <summary>
		/// Sets the current reader that this object uses to read data
		/// </summary>
		/// <param name="reader">IDataReader that will be used for further operations</param>
		public void SetReader(System.Data.IDataReader reader)
		{
			_reader = reader;
			_row = null;
		}

		public IDataReader DataReader
		{
			get { return _reader; }
		}

		/// <summary>
		/// Sets the current DataRow that this object uses to read data
		/// </summary>
		/// <param name="row">DataRow that will be used for futher operation</param>
		public void SetRow(System.Data.DataRow row)
		{
			_row = row;
			_reader = null;
		}

		/// <summary>
		/// Indexer to retrieve a value by numeric index
		/// </summary>
		public object this[int index]
		{
			get
			{
				if (_reader != null)
					return _reader.GetValue(index);
				else if (_row != null)
					return _row[index];

				return null;
			}
		}

		/// <summary>
		/// Indexer to retrieve a value by numeric index
		/// </summary>
		public object this[string index]
		{
			get
			{
				if (_reader != null)
					return _reader[index];
				else if (_row != null)
					return _row[index];

				return null;
			}
		}

		/// <summary>
		/// Resets the column map cache
		/// </summary>
		public void ResetColumnMap()
		{
			_columnMap = null;
		}

		/// <summary>
		/// Gets the field count
		/// </summary>
		public int FieldCount
		{
			get
			{
				if (_reader != null)
					return _reader.FieldCount;
				else if (_row != null)
					return _row.ItemArray.Length;

				return 0;
			}
		}

		/// <summary>
		/// Builds an internal column map cache so that large numbers of records
		/// can quickly search to see if the column exists. SqlDataReader throws 
		/// an exception when attempting to GetOrdinal on a non-existant column
		/// </summary>
		private void BuildColumnMap()
		{
			if (_columnMap != null)
				return;

			_columnMap = new Dictionary<string, int>();
			for (int col = 0; col < _reader.FieldCount; col++)
				_columnMap.Add(_reader.GetName(col).ToLower(), col);
		}

		/// <summary>
		/// Gets whether a field exists
		/// </summary>
		/// <param name="fieldName">Field name to check</param>
		/// <returns>True if the field exists, otherwise false</returns>
		public bool FieldExists(string fieldName)
		{
			if (_reader != null)
			{
				BuildColumnMap();
				return _columnMap.ContainsKey(fieldName.ToLower());
			}
			else if (_row != null)
				return _row.Table.Columns.Contains(fieldName);

			return false;
		}

		/// <summary>
		/// Retrieves the ordinal position of the field
		/// </summary>
		/// <param name="fieldName">Field name to retrieve</param>
		/// <returns>Index of the field if it exists</returns>
		/// <exception cref="IndexOutOfRangeException">Thrown if the field does not exist</exception>
		public int GetOrdinal(string fieldName)
		{
			if (_reader != null)
				return _reader.GetOrdinal(fieldName);
			else
			{
				int index = _row.Table.Columns.IndexOf(fieldName);
				if (index < 0)
					throw new IndexOutOfRangeException("Field '" + fieldName + "' does not exist.");
				return index;
			}
		}

		/// <summary>
		/// Gets whether the column at the specified index is null
		/// </summary>
		/// <param name="column">Index of the field to check</param>
		/// <returns>True if the field is null</returns>
		public bool IsFieldNull(int column)
		{
			if (_reader != null)
				return _reader.IsDBNull(column);
			else if (_row != null)
				return _row.IsNull(column);

			return true;
		}

		/// <summary>
		/// Gets whether the column with the specified name is null
		/// </summary>
		/// <param name="fieldName">Name of the field to check</param>
		/// <returns>True if the field is null</returns>
		public bool IsFieldNull(string fieldName)
		{
			if (_reader != null)
			{
				BuildColumnMap();
				return _reader.IsDBNull(_columnMap[fieldName]);
			}
			else if (_row != null)
				return _row.IsNull(fieldName);

			return true;
		}

		#region //****** Field Handlers (for common types) ******//
		public T GetField<T>(int index)
		{
			object fieldData = this[index];

			if (fieldData == DBNull.Value)
				return default(T);

			// If this is a value type, this will be boxed, and therefore an explicit cast will not work if the types do not match exactly.
			// Use System.Convert.ChangeType to use use registered type conversions.
			return (T)System.Convert.ChangeType(fieldData, typeof(T));
		}

		public object GetField(int index)
		{
			return this[index];
		}

		public T? GetNullableField<T>(int index) where T: struct
		{
			object fieldData = this[index];

			if (fieldData == DBNull.Value)
				return new T?();

			// If this is a value type, this will be boxed, and therefore an explicit cast will not work if the types do not match exactly.
			// Use System.Convert.ChangeType to use use registered type conversions.
			return (T)System.Convert.ChangeType(fieldData, typeof(T));
		}

		public T GetField<T>(string fieldName)
		{
			object fieldData = this[fieldName];

			if (fieldData == DBNull.Value)
				return default(T);

			// If this is a value type, this will be boxed, and therefore an explicit cast will not work if the types do not match exactly.
			// Use System.Convert.ChangeType to use use registered type conversions.
			return (T)System.Convert.ChangeType(fieldData, typeof(T));
		}

		public T? GetNullableField<T>(string fieldName) where T: struct
		{
			object fieldData = this[fieldName];

			if (fieldData == DBNull.Value)
				return new T?();

			// If this is a value type, this will be boxed, and therefore an explicit cast will not work if the types do not match exactly.
			// Use System.Convert.ChangeType to use use registered type conversions.
			return (T)System.Convert.ChangeType(fieldData, typeof(T));
		}

		public bool GetBool(string fieldName)
		{
			return GetField<bool>(fieldName);
		}

		public bool? GetNullableBool(string fieldName)
		{
			return GetNullableField<bool>(fieldName);
		}

		public byte GetByte(string fieldName)
		{
			return GetField<byte>(fieldName);
		}

		public byte? GetNullableByte(string fieldName)
		{
			return GetNullableField<byte>(fieldName);
		}

		public short GetInt16(string fieldName)
		{
			return GetField<short>(fieldName);
		}

		public short? GetNullableInt16(string fieldName)
		{
			return GetNullableField<short>(fieldName);
		}

		public int GetInt32(string fieldName)
		{
			return GetField<int>(fieldName);
		}

		public int? GetNullableInt32(string fieldName)
		{
			return GetNullableField<int>(fieldName);
		}

		public long GetInt64(string fieldName)
		{
			return GetField<long>(fieldName);
		}

		public long? GetNullableInt64(string fieldName)
		{
			return GetNullableField<long>(fieldName);
		}

		public decimal GetDecimal(string fieldName)
		{
			return GetField<decimal>(fieldName);
		}

		public decimal? GetNullableDecimal(string fieldName)
		{
			return GetNullableField<decimal>(fieldName);
		}

		public double GetDouble(string fieldName)
		{
			return GetField<double>(fieldName);
		}

		public double? GetNullableDouble(string fieldName)
		{
			return GetNullableField<double>(fieldName);
		}

		public string GetString(string fieldName)
		{
			return GetField<string>(fieldName) ?? string.Empty;
		}

		public string GetNullableString(string fieldName)
		{
			return GetField<string>(fieldName);
		}

		public DateTime GetDateTime(string fieldName)
		{
			return GetField<DateTime>(fieldName);
		}

		public DateTime? GetNullableDateTime(string fieldName)
		{
			return GetNullableField<DateTime>(fieldName);
		}

		public byte[] GetBytes(string fieldName)
		{
			return GetField<byte[]>(fieldName) ?? new byte[0];
		}

		public byte[] GetNullableBytes(string fieldName)
		{
			return GetField<byte[]>(fieldName);
		}

		public Guid GetGuid(string fieldName)
		{
			return GetField<Guid>(fieldName);
		}

		public Guid? GetNullableGuid(string fieldName)
		{
			return GetNullableField<Guid>(fieldName);
		}
		#endregion //****** Field Handlers (for common types) ******//

		#region /***** Casting operators *****/
		public static implicit operator RowReader(DataRow row)
		{
			return new RowReader(row);
		}
		#endregion /***** Casting operators *****/
	}
}

